/*****************************************************************************
 *  CommLayerLib - CommLayer.java
 *  Copyright (C) 2012  Matteo Piscitelli
 *  E-mail: matteo@picciux.it
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  For further info, check http://code.google.com/p/java-comm-layer/
 *
 *  SVN: $Id: CommLayer.java 41 2012-10-11 16:35:35Z picciux@gmail.com $
 *  
 *****************************************************************************/

package it.picciux.commlayer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import it.picciux.commlayer.log.Logger;
import it.picciux.commlayer.log.LoggerException;

public abstract class CommLayer implements ICommLayer {
	public abstract class DataReader extends Thread {
		
	    private boolean keepRunning = false;
		protected int disconnectCause = -1;
		protected InputStream in;
		protected CommLayer layer;
		
		public DataReader() {
		}
		
	    public DataReader(InputStream inputStream) {
	    	this.in = inputStream;
	    }
	    
	    public synchronized void terminate() {
	    	if (disconnectCause == -1)
	    		disconnectCause = CommLayer.DISCONNECTED;
	   		keepRunning = false;
	    }

		public synchronized boolean isRunning() {
			return keepRunning;
		}    
		
		protected void startDataLoggers() {
			/* 
			 * trying to open registered dataloggers: copying them in a temp array,
			 * testing open success, and re-add to the list if successful
			 */
			DataLogger[] loggers = new DataLogger[dataLoggers.size()];
			dataLoggers.copyInto(loggers);
			dataLoggers.removeAllElements();
			for (int i = 0; i < loggers.length; i++)
				if (loggers[i].openLog())
					dataLoggers.addElement(loggers[i]);

			loggers = null;		
		}
		
		protected void stopDataLoggers() {
	        // ask registered data loggers to close their logs
	    	for (int i = 0; i < dataLoggers.size(); i++)
	    		if (dataLoggers.elementAt(i) != null)
	    			((DataLogger) dataLoggers.elementAt(i)).closeLog();        		
		}
		
		/**
		 * Starts the DataReader thread, providing and InputStream to read data from.
		 * @param inputStream
		 */
		public void start(InputStream inputStream) {
			if (isRunning()) return;
			this.in = inputStream;
			start();
		}
		
		/**
		 * Starts the DataReader thread. Note that, before starting the thread, program 
		 * must provide an InputStream to read data from, calling {@link DataReader#setInputStream(InputStream)}
		 */
		public void start() {
			if (isRunning()) return;
			setThreadData();
			keepRunning = true;
			inputBuffer = new byte[inputBufferSize];
			super.start();
		}
		
		public void run() {
			startDataLoggers();
			
	        while (isRunning()) {
	        	
				try {
					dataAvailable = in.read(inputBuffer);
				} catch (IOException e) {
					terminate();
					break;
				}
				
				if (dataAvailable == -1) {
					disconnectCause = CommLayer.EOF;
					terminate();
					break;
				}
				
				if (dataAvailable > 0) {

					notifyListeners(CommLayer.DATA_AVAILABLE, new Integer(dataAvailable));
					
					if (dataLoggers.size() > 0) {
						for (int i = 0; i < dataLoggers.size(); i++) {
							((DataLogger) dataLoggers.elementAt(i)).writeData(inputBuffer, 0, dataAvailable);
						}
					}
				} 
				
	        }
	        
	        stopDataLoggers();
	        
	        inputBuffer = null;
	        dataAvailable = 0;
	        
	        // signal disconnection to CommLayer here
	        if (disconnectCause == -1) disconnectCause = CommLayer.DISCONNECTED;
			notifyListeners(disconnectCause);
		}


		public void setInputStream(InputStream inputStream) {
			this.in = inputStream;
		}
		
		public abstract void setThreadData();
	}
	
	public abstract class Settings {
		protected int fieldIndex;
		
		
		/* config management */
		public abstract boolean setConfigValue(ConfigValue v);	
		
		public abstract ConfigValue getNextConfig();

		public void configRewind() {
			fieldIndex = 0;
		}
		
		public abstract Settings clone();
	}
	
	protected Settings settings;
	protected DataReader dataReader = null;
	protected DataWriter dataWriter = null;
	protected InputStream in;
	protected OutputStream out;
	protected Vector listeners;
	protected Vector dataLoggers;
	
	protected boolean connected = false;
	protected boolean notifyOnDataAvailable = true;
	protected boolean writeOnSeparateThread = true;

	public static final int DEFAULT_BUFFER_SIZE = 1024;
	
	protected int inputBufferSize = DEFAULT_BUFFER_SIZE;
	protected byte[] inputBuffer;
	protected int dataAvailable = 0;
	
	protected int outputBufferSize = DEFAULT_BUFFER_SIZE;
	
	public static final int DISCONNECTED 			= 0;
	public static final int CONNECTION_FAILED 		= 1;
	public static final int CONNECTION_LOST 		= 2;
	//public static final int NOSUCHDEV				= 3;
	public static final int NOCONFIG 				= 3;
	public static final int CONNECTION_BUSY 		= 4;
	public static final int CONNECTION_ERROR 		= 5;
	public static final int PORTSCAN_STARTED 		= 6;
	public static final int PORTSCAN_COMPLETED 		= 7;
	public static final int PORTSCAN_ERROR 			= 8;
	public static final int PORTSCAN_CANCELED 		= 9;
	public static final int PORTSCAN_PORT_FOUND 	= 10;
	public static final int EOF 					= 11;
	public static final int WRITE_FAILED			= 12;

	public static final int CONNECTION_STARTED		= 100000;
	public static final int CONNECTED 				= 100001;
	public static final int DATA_AVAILABLE 			= 100002;
	public static final int WRITE_COMPLETED 		= 100003;
	
	public static final int SERIAL	 	= 1;
	public static final int NETWORK		= 2;
	public static final int BLUETOOTH	= 3;
	public static final int FILE 		= 4;
	
	private static Logger log;
	private static int LOGLEVEL = Logger.OFF;
	
	
	private void setLogger() {
		final String logName = "it.picciux.commlayer";
		
		try {
			log = Logger.getLogger(logName, Logger.CONSOLE, null);
		} catch (LoggerException e) {
			log = Logger.getNullLogger(logName);
		}
		
		log.setLevel(LOGLEVEL);
	}
	
	public CommLayer() {
		listeners = new Vector();
		dataLoggers = new Vector();
		setLogger();
	}

	/*
	@SuppressWarnings("rawtypes")
	public CommLayer(DataReader dataReader) {
		listeners = new Vector();
		if (dataReader != null)
			setDataReader(dataReader);
		setLogger();
	}

	@SuppressWarnings("rawtypes")
	public CommLayer(DataReader dataReader, DataWriter dataWriter) {
		listeners = new Vector();
		if (dataReader != null)
			setDataReader(dataReader);
		if (dataWriter != null)
			setDataWriter(dataWriter);
		setLogger();
	}
	*/
	
	public Settings getSettings() {
		return settings;
	}

	public void setSettings(Settings settings) {
		this.settings = settings;
	}

	public int[] getBaudRateValues() {
		return null;
	}

	public int[] getDataBitsValues() {
		return null;
	}

	public int[] getParityValues() {
		return null;
	}

	public int[] getStopBitsValues() {
		return null;
	}

	public int[] getFlowControlValues() {
		return null;
	}

	public String getParityValue2String(int value) {
		return null;
	}

	public String getFlowControlValue2String(int value) {
		return null;
	}

	public int getParityString2Value(String s) {
		return 0;
	}

	public int getFlowControlString2Value(String s) {
		return 0;
	}

	public boolean scan() {
		return false;
	}
	
	public void cancelScan() {
	}
	
	public CommLayerPort[] getPortList() {
		return null;
	}

	public boolean addEventListener(ICommEventListener listener) {
		listeners.addElement(listener);
		return true;
	}
	
	public boolean removeEventListener(ICommEventListener listener) {
		return listeners.removeElement(listener);
	}

	protected void notifyListeners(int status) {
		notifyListeners(status, null);
	}
	
	protected void notifyListeners(int status, Object extraData) {
		
		connected = ( status >= CONNECTED );
		
		for (int i = 0; i < listeners.size(); i++) {
			ICommEventListener l = (ICommEventListener) listeners.elementAt(i);
			l.commLayerEvent(status, extraData);
		}
	}
	
	public boolean isConnected() {
		return connected;
	}

	/**
	 * @return the in
	 */
	public InputStream getInputStream() {
		return in;
	}

	/**
	 * @return the out
	 */
	public OutputStream getOutputStream() {
		return out;
	}
	
	public DataWriter getDataWriter() {
		if (writeOnSeparateThread)
			return dataWriter;
		else
			return null;
	}
	
	/**
	 * Descendants of CommLayer must implement this method to actually do any initialization
	 * and operation to connect to the desired end point. It's this method responsibility to
	 * provide an {@link InputStream} and an {@link OutputStream} for the endpoint upon 
	 * successful connection.
	 * @throws CommLayerException
	 */
	protected abstract void tryConnection() throws CommLayerException;
	
	/**
	 * 
	 */
	protected abstract void doDisconnect();
	
	protected abstract void setConnectThread(Thread thread);
	
	public boolean connect() throws CommLayerException {
		if (connected) return true;
		
		/*
		if (dataReader == null) {
        	throw new CommLayerException("No data reader");
        }
		*/
		
		Thread t = new Thread(new Runnable() {
			
			public void run() {
				notifyListeners(CONNECTION_STARTED);
				
				try {
					tryConnection();
				} catch (CommLayerException e) {
					notifyListeners(e.getCode(), e.getMessage());
					return;
				}
				
				if (dataReader != null) {
					dataReader.start(getInputStream());
				}
					
				if (dataWriter != null) {
					dataWriter.setEventListener(new ICommEventListener() {
						public void commLayerEvent(int status, Object extraData) {
							notifyListeners(status, extraData);
						}
					});
					
					dataWriter.start(getOutputStream());
				}
					
				connected = true;
				notifyListeners(CONNECTED);
			}
		});
        
		setConnectThread(t);
		t.start();
		
        return true;
	}
	
	public void disconnect() {
		if (! connected) {
			return;
		}

		if (dataReader != null) {
			dataReader.terminate();
		}
		
		if (dataWriter != null)
			dataWriter.terminate();
		
		doDisconnect();
		
		connected = false;
		
		if (dataReader == null) {
			notifyListeners(DISCONNECTED);			
		}
		
		dataReader = null;
		dataWriter = null;
	}
	
	public byte[] getData() {
		if (dataReader != null)
			return inputBuffer;
		else
			return null;
	}

	/**
	 * @return the notifyOnDataAvailable
	 */
	public boolean isNotifyOnDataAvailable() {
		return notifyOnDataAvailable;
	}

	/**
	 * @param notifyOnDataAvailable the notifyOnDataAvailable to set
	 */
	public void setNotifyOnDataAvailable(boolean notifyOnDataAvailable) {
		if (! connected)
			this.notifyOnDataAvailable = notifyOnDataAvailable;
	}

	/**
	 * @return the bufferSize
	 */
	public int getBufferSize() {
		return inputBufferSize;
	}

	/**
	 * @param bufferSize the bufferSize to set
	 */
	public void setInputBufferSize(int bufferSize) {
		this.inputBufferSize = bufferSize;
	}
	
	public int addDataLogger(IDataWriter dataLogger) {
		if (dataLogger == null) return -1;
		
		dataLoggers.addElement(dataLogger);
		return dataLoggers.size() - 1;
	}
	
	public boolean removeDataLogger(int index) {
		if (index < 0 || index > dataLoggers.size() - 1) return false;
		dataLoggers.removeElementAt(index);
		return true;
	}
	
	public void removeAllDataLoggers() {
		dataLoggers.removeAllElements();
	}
	
	public boolean removeDataLogger(IDataWriter dataLogger) {
		return dataLoggers.removeElement(dataLogger);
	}

	/**
	 * @return the writeOnSeparateThread
	 */
	public boolean isWriteOnSeparateThread() {
		return writeOnSeparateThread;
	}

	/**
	 * @param writeOnSeparateThread the writeOnSeparateThread to set
	 */
	public void setWriteOnSeparateThread(boolean writeOnSeparateThread) {
		if (! connected)
			this.writeOnSeparateThread = writeOnSeparateThread;
	}

	/**
	 * @return the outputBufferSize
	 */
	public int getOutputBufferSize() {
		return outputBufferSize;
	}

	/**
	 * @param outputBufferSize the outputBufferSize to set
	 */
	public void setOutputBufferSize(int outputBufferSize) {
		this.outputBufferSize = outputBufferSize;
	}
	
	
}
